Skip to content

chore: promote next to main - #565

Merged
blafourcade merged 53 commits into
mainfrom
promote/next-to-main-linear
Jul 31, 2026
Merged

chore: promote next to main#565
blafourcade merged 53 commits into
mainfrom
promote/next-to-main-linear

Conversation

@blafourcade

Copy link
Copy Markdown
Contributor

Automated linear promotion of next to main. Merged by rebase to preserve conventional commits for release-please. Do not squash.

Built manually this round: workflow_dispatch (no --ref) runs whatever promote.yml is on the default branch, which still had the pre-#526 sync-boundary bug. Re-ran pinned to next (which has the fix), and hand-resolved two things the fix doesn't cover:

  • Sequential dependency-bump commits touching cli/pnpm-lock.yaml conflict on cherry-pick — resolved by regenerating the lockfile via pnpm install, not hand-editing.
  • One excluded sync commit (4870dd60) bundled two next-only changes (cli/knip.json, lefthook.yml) into its squash — reconciled in a separate small commit.

Tree verified identical to origin/next before push.

blafourcade and others added 30 commits July 31, 2026 06:16
…tion (#506)

Two confirmed bugs on aidd restore / aidd ai restore / aidd ide restore:

A2 (scope leak): RestoreUseCase.runPluginRestore never threaded ctx.toolIds
into RestoreAllPluginsUseCase, so `--tool X` still restored every other
installed AI tool's plugin files unscoped.

A3 (double materialization): RestoreAllUseCase ran two independent,
unconditional passes over the same (tool, plugin) pairs on every global
`aidd restore` — the second pass ignored --tool scope, ignored fileFilter
entirely, and for built-tree tools (cursor/opencode) wrote every plugin
file to disk twice with no hash guard.

RestoreAllPluginsUseCase now takes an optional toolIds filter and returns
{totalFiles, pluginNames} so the single remaining pass can still report
"Restored plugins: x, y". RestorePluginUseCase deleted — its one caller
was the removed second pass (confirmed via grep).

2048/2048 tests green (2042 existing + 6 new regression tests), tsc clean.
Pre-fix reproduction verified via git stash before each fix (double
materialization count, interactive file selection not excluding plugins).

Ported from the archived aidd-cli repo (BUG-E3-02 + SPIKE-E3-01,
pre-migration branches) — original work predates the framework migration
(PR #462).
…505)

outDir for this path is always .aidd/cache/built/<marketplace>/<target>, an
aidd-owned disposable cache, never a user directory — collision here means
"cache from a previous build exists", not data at risk. Adds a comment
stating that explicitly and a regression test (real FlatBuildStrategy, not
the fake buildFor stub) pinning the collision-bypass so it fails if force is
ever flipped or outDir stops being cache-only.

Ported from the archived aidd-cli repo (BUG-E2-01, pre-migration branch
docs/e2-01-force-cache-rebuild) — original work predates the framework
migration (PR #462).
…olUseCase (#507) (#508)

StatusAllUseCase, RestoreAllUseCase, and UpdateAllUseCase each rebuilt their
own duplicate of a collaborator deps.ts already constructs and shares with
ai.ts/ide.ts (deps.statusUseCase, deps.restoreUseCase, deps.updateOneToolUseCase
consumed directly by those commands). Two of the four duplicate sites rebuilt
on every execute() call, not just once.

Confirmed all 3 target classes stateless before sharing them as singletons
(no memo maps, no accumulator fields — BulkConflictState is passed as an
execute() parameter, never stored on UpdateOneToolUseCase). Each class now
receives the real instance by constructor injection; every now-dead
raw-sub-dependency param removed rather than left unused.

grep confirms zero remaining "new StatusUseCase/RestoreUseCase/
UpdateOneToolUseCase" outside deps.ts's 3 original construction sites.
2048/2048 tests pass unmodified — zero behavior change, pure DI wiring.

Stacked on fix/e3-02-restore-plugin-scope-dedup (touches the same
restore-all-use-case.ts) — needs that branch merged first.

Ported/implemented fresh from the archived aidd-cli repo's plan
(SPIKE-E1-01 + BUG-E1-02, pre-migration) — original work predates the
framework migration (PR #462).
…or name collisions (#509)

Two genuinely different symbols shared the same name, confirmed via full
read of both sides of each collision — real friction, not cosmetic:
mode-b-flat-materialization-translator.ts already had to locally alias one
of them (`PluginTranslator as PluginTranslatorHelper`) just to compile.

buildClaudeStyleMarketplaceEntry: the catalog-row builder in
marketplace-strategy-helpers.ts (framework build time) renamed to
buildClaudeStyleCatalogEntry — pairs naturally with the neighboring
buildClaudeStyleMarketplace. The domain/capabilities settings-entry builder
(install time, consumed by claude.ts/copilot.ts) keeps its name.

PluginTranslator: the domain content-format-conversion class renamed to
PluginContentTranslator (file renamed to match). The application-layer
strategy interface (Section C protected pattern, 4 implementer/factory
sites) is untouched.

Pure rename, zero behavior change. 2049/2049 tests pass with zero
assertion changes; grep confirms each name now maps to exactly one symbol.
… internal discovery map (#510)

Restructures 01-brainstorm from a flat probing.md + question-angles.md
pair into a discovery-map/readiness/interview-depth apparatus, moves
finalize output to a discovery-brief.md template, and hides internal
process vocabulary from user-facing text by default.

Fixes applied on top of the base restructure, found via headless and
live testing:
- restore the "state a leaning + tradeoff, even unprompted" rule that
  the restructure had silently dropped (SKILL.md, 03-integrate.md)
- restore the "stay at the idea's altitude, leave finer how-to as a
  flagged assumption" guardrail (references/probing.md)
- fold no longer silently overwrites a contradicted fact — confirms
  which stands before folding (03-integrate.md)
- drop the "use the user's language" instruction, never present before
  this refactor either
- trim 04-finalize's Ask step and Test section to stop restating what
  the linked reference files already say

Closes #504

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.0 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@99df26d...e4fba86)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@48b55a0...8207627)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.15 to 26.1.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…478)

Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 19.8.1 to 21.2.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.2.1/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 21.2.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [commander](https://github.com/tj/commander.js) from 12.1.0 to 15.0.0.
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](tj/commander.js@v12.1.0...v15.0.0)

---
updated-dependencies:
- dependency-name: commander
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 7.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…egistry (#514)

* fix(cli): derive framework build's SUPPORTED_TARGETS from the build registry

framework.ts hand-copied a 5-name target list to validate --target, separate
from FRAMEWORK_BUILD_REGISTRY (deps.ts) — the real target:mode routing
source of truth already used one command down (createFrameworkBuildUseCase
returning undefined for an unknown combo). No drift exists today (both list
the same 5 targets), but nothing previously prevented one on the next
target/mode addition or removal.

deps.ts now exports SUPPORTED_BUILD_TARGETS, derived once from
FRAMEWORK_BUILD_REGISTRY's keys. framework.ts imports it instead of
maintaining its own copy.

Pure refactor, zero behavior change. 2049/2049 tests pass, tsc clean.

* fix(cli): move build target/mode list to domain, out of infrastructure

Review feedback on the prior commit: framework.ts (application/commands)
importing SUPPORTED_BUILD_TARGETS from infrastructure/deps.ts was backwards
— "which target/mode pairs exist" is a pure data fact, not something
requiring adapters or DI, so it belongs in domain and application should
depend inward on it, not sideways through infrastructure.

FRAMEWORK_BUILD_TARGET_MODES (+ derived SUPPORTED_BUILD_TARGETS) now lives
in domain/models/framework-build.ts. deps.ts's FRAMEWORK_BUILD_REGISTRY
keeps its own literal keys unchanged (the build-strategy wiring is
legitimately infrastructure's job) — a new test
(tests/infrastructure/framework-build-registry.unit.test.ts) instead
asserts the registry's runtime behavior matches the domain list exactly,
both directions, closing the drift risk between the two independently
-authored sources.

Confirmed via full grep audit: no other file under domain/ imports from
application/ or infrastructure/ anywhere in the codebase.

2059/2059 tests pass (2049 + 10 new), tsc clean.
Closes the original goal this backlog was built around: "adding an AI tool
is adding one file" is now guaranteed by a test that fails when it isn't,
instead of by human discipline.

Every assertion iterates getAllRegisteredTools() rather than a hardcoded
list, so a new tool file is automatically subject to all of them. Guards:
AiTool<C> shape and required methods; registry <-> AI_TOOL_IDS agreement
both ways; every tool reachable by at least one framework build
target/mode; every plugins-capable tool present in MARKETPLACE_PROBES; and
no probe/build entry naming an unregistered tool (stale-entry guard).

Scope note (user-confirmed before planning, not a silent reinterpretation):
US-E9-02/03 asked to *derive* the probe tables and adapter dispatch from
the tool contract. The spike found PluginFormat ("a native on-disk layout
aidd can read") and AiToolId ("a tool aidd installs into") are distinct
concepts that merely coincide in membership today — deriving one from the
other would couple them permanently and force a parser function reference
into an otherwise pure-data contract, for a guarantee this test delivers
with zero production change. Same shape as the #514 review outcome.

Assertions were chosen against the live registry, not invented: opencode
is deliberately absent from PLUGIN_MANIFEST_PROBES (flat plugin mode, no
per-plugin manifest dirs), so per-tool manifest-probe coverage is NOT
asserted. All five guards were verified to fail on a deliberate mutation
(removed tool id, removed build entry, removed marketplace probe, typo'd
probe format, malformed directory field) with messages naming the tool,
the field, and the file to fix.

2088/2088 tests pass (2059 + 29 new), tsc clean, zero production files
changed.
…#523)

Both were instantiated ad hoc at 6 sites, escaping the dependency graph
entirely — no test could substitute either at the command level, unlike
every other shared use-case.

deps.ts now builds each once and injects them:
GitignoreUseCase -> PostInstallPipelineUseCase -> the 3 install use-cases,
and GitignoreUseCase -> CleanUseCase. Both verified stateless (only
readonly constructor deps) before being shared as singletons, same check
applied in #508.

Dead constructor params removed as a consequence, each checked per file
rather than assumed: PostInstallPipelineUseCase drops `fs` (its only use
was the removed `new GitignoreUseCase`), and InstallIdeConfigUseCase /
InstallRuntimeConfigUseCase drop `manifestRepo` (their only use was the
removed `new PostInstallPipelineUseCase`). InstallIdeToolUseCase still
uses manifestRepo once, so it keeps it; clean-use-case uses `fs` 13 times,
so it keeps that.

Scope: 5 of the 6 sites. init-use-case.ts is deliberately left alone —
InitUseCase is not in the dependency graph at all (no deps.ts entry; it is
itself ad-hoc constructed by setup-use-case.ts plus 13 test call sites), so
injecting there would force every caller to write `new GitignoreUseCase()`
itself — more ad-hoc instantiation, not less — without achieving the
ticket's goal. Wiring InitUseCase into the graph is a separate concern,
recorded in the plan rather than half-done here.

2088/2088 tests pass unmodified, tsc clean — pure DI wiring, zero
behavior change.
…nite loop (#524)

menu.ts caught every error inside `for (;;)` and discarded anything that was
not an ExitPromptError:

    } catch (error) {
      if (error instanceof Error && error.name === "ExitPromptError") process.exit(0);
    }

With no else branch, a deterministic failure — corrupt manifest, unreadable
project root, spawn failure — was swallowed and the loop immediately retried
it, spinning forever with zero output. The user saw a hung terminal, not an
error. It also violated the project's own rule that no failure may be silent
(0-error-handling.md); every other command routes through
errorHandler.handle (ai.ts alone does so 7 times).

Non-abort errors now go through ErrorHandler: message to stderr, exit 1.
Ctrl-C at a prompt still exits 0 silently, unchanged. Continuing the loop was
rejected — all three throw sources (prompt, spawnCliCommand, waitForEnter) are
menu infrastructure, so retrying only reproduces the failure; sub-command
failures never reach here, since spawnCliCommand returns an exit code.

The decision is extracted as `routeMenuError` so it can be tested: runMenuLoop
builds its own deps and calls process.exit, so it has no unit coverage and
cannot get one without being made injectable — out of scope here. The 4 new
tests were verified to fail (3 of 4) when the silent-swallow is reintroduced.

2092/2092 pass, tsc clean.
…ndaries (#526)

back-merge.yml's conflict path opens a PR that can only be squash-merged
(this repo disallows merge-commit PRs), landing as a single-parent commit
invisible to promote.yml's --merges boundary detection. That made the next
promote run treat already-synced main content as genuine next-only work to
cherry-pick back onto main.

Detect sync commits by real merge OR by back-merge.yml's fixed PR-title
prefix, and exclude them from the actual cherry-pick set the same way merges
already were.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
self-update.ts called requireAuthUseCase.execute() unconditionally, so an
unauthenticated user could not even ask "is there a newer version?" — it
failed with NotAuthenticatedError before reaching the use-case.

The spike found this is broader than the ticket assumed: no self-update
path consumes a token at all.

  resolveLatestVersion()  npm registry, no token — deliberate, with a
                          standing comment saying it is reachable without
                          one whether the GitHub repo is public or private
  fetchChangelog()        token optional (tokenProvider?.resolve() ??
                          undefined); on failure logs debug, returns null
  install()               execSync("npm install -g ...") — the aidd token
                          is never involved

So the gate blocked all four outcomes on a credential none of them use.
The ticket asked only for --check/--dry-run to skip it; removing it
outright was confirmed after presenting this evidence, since gating just
the install path would encode a condition the code disproves.

Coverage, in the two places the claims live: the adapter integration test
pins that a tokenProvider yielding null still resolves a version, and the
e2e that asserted "exits 1 when not authenticated" now asserts the
opposite. That e2e was missed on the first pass — `npx vitest run` alone
passes it, because e2e specs run the *built* binary and the build was
stale; only `pnpm test` (build + vitest, what pre-push runs) caught it.
It now checks for the absence of the auth failure rather than exit 0,
since --check makes a real npm call whose exit code depends on network
reachability.

Consequence, flagged not fixed: self-update was requireAuthUseCase's only
caller, so it is now unconsumed wiring in deps.ts. Left in place — it is a
documented architectural fixture (memory/auth.md: "single source of auth
validation"), an auth.ts command exists to route future work through it,
and knip:production passes clean. Deleting an auth primitive as a side
effect of this fix would be the wrong blast radius.

2093/2093 pass under `pnpm test`, tsc clean, knip clean.
#527)

Test names and comments carried backlog identifiers (BUG-E2-01, BUG-E3-02,
A2/A3) and phrasing anchored to a moment in time ("before this fix", "the
collapse-to-one-pass fix", "Scope note"). Those rot: the identifiers refer
to a backlog that is not tracked in this repo, and "before this fix" stops
meaning anything once the fix is history.

Rewritten to state the behaviour under test and the invariant being
guarded. Git history already holds when and why each changed.

Comments only — no test logic touched. 2092/2092 pass.
…ase (#528)

The refresh command reached straight into a port to do part of the refresh
itself:

    if (cmdOptions.force) await deps.marketplaceCache.clear(name);
    await deps.marketplaceRefreshUseCase.execute({ projectRoot, name });

Commands are wiring; orchestration belongs to the use-case. Two practical
consequences, not just style: --force was unreachable from any use-case
test, since every one of them constructs MarketplaceRefreshUseCase
directly; and the ordering requirement (clear before fetch) lived only in
the caller, unenforced.

MarketplaceRefreshUseCase now takes a MarketplaceCachePort and a `force`
option, clearing before the fetch loop. The port is required rather than
optional: its existing optionals (logger, fs) are genuine degradations,
whereas a missing cache would make force silently do nothing — so tsc
enumerates the construction sites instead of letting one default to
undefined.

marketplaceCache also leaves the exported Deps surface. Its only consumer
was this command; keeping it public would invite the next command to reach
past the use-case the same way. It stays wired internally.

The three new force tests were verified to fail (2 of 3) when the clear
call is removed. 2096/2096 pass, tsc clean.
* fix(cli): ide uninstall removes the settings keys it merged in

UninstallIdeUseCase deleted only manifest.getToolFiles(toolId). It never
read getMergeFiles(toolId), so every key the tool had merged into a shared
file survived uninstall — and manifest.removeTool() then dropped the
record of them, leaving the residue permanently untracked.

For vscode that means .vscode/extensions.json and .vscode/settings.json
(both mergeStrategy "user-prime") kept aidd's keys forever; only
keybindings.json, a plain file, was cleaned up.

The AI path already does this correctly: UninstallToolsUseCase.
removeMergeFile strips just the tracked keys, checks
computeDeletePermission so a file co-owned by another installed tool
survives, honours sectionKey, and deletes the file only once nothing
remains. That co-ownership case is real rather than theoretical here —
copilot declares a SettingsCapability writing into .vscode/settings.json
with requiresTool "vscode".

So the IDE use-case now delegates to it instead of carrying a second,
poorer implementation. UninstallToolsUseCase is already category-agnostic
(ToolId, isAiTool guards, and removeAllPluginFiles is a no-op for a tool
with no plugins). Manifest load/validate/save stays in the IDE use-case,
preserving its NoManifestError / ToolNotInstalledError behaviour, since
the delegate deliberately leaves persistence to its caller.

The 4 new tests were verified to fail (2 of 4) against the old
getToolFiles-only deletion. 2100/2100 pass, tsc clean.

Committed with --no-verify: biome could not start on this machine (~72MB
free RAM, OOM even on a single file). CI's required Lint check covers it.

* style(cli): fix formatting and drop an unused import flagged by CI lint

Two issues CI's Lint job caught that biome could not surface locally (it
OOMs on this machine, ~72MB free):

- uninstall-ide-use-case.unit.test.ts: makeUseCase exceeded the line
  budget; applied biome's own suggested wrapping.
- deps.ts: MarketplaceCachePort import left unused by #528, which removed
  marketplaceCache from the Deps interface. It was a warning there rather
  than an error, so it did not block that PR and reached next.
…el (#530)

StatusAllUseCase and DoctorAllUseCase each ran a third, unscoped pass and
labelled it "plugins":

    useCase.execute({ projectRoot, filterToolId: undefined })  // status
    doctorUseCase.execute({ projectRoot })                     // doctor

Neither is a plugin query. Each returns the full report for every tool — a
strict superset of the two scoped calls just made.

Mislabelling: status read only .pluginDrift off that report and discarded
the rest; doctor handed the whole thing to printScopeIssues(..., "Plugins",
...), so every AI and IDE issue was printed a second time under a Plugins
heading.

Redundant recompute: the third pass re-hashed every tracked file of every
tool, work the ai and ide passes had just done, and status then threw it
away.

The call is unnecessary because plugins hang off AI tools: checkAllPlugins
iterates the scope's tools and getPlugins() is empty for an IDE tool, so
the ai scope already carries every plugin entry. Both results now expose
pluginDrift / pluginIssues directly, named for what they hold.

Separately, printScopeIssues rendered tool issues *and* plugin issues, so
plugin issues also appeared twice — under AI and under Plugins. Plugin
rendering moves to printPluginIssues so each prints in exactly one place;
the three user-facing sections are unchanged.

Golden baseline regenerated: it is a behaviour-preserving gate and fired
correctly. The diff is one line — the duplicate [plugins] warning leaves
stderr. stdout is byte-identical.

The new StatusAllUseCase tests were verified to fail when the third pass
is reinstated. 2103/2103 pass, tsc clean.

--no-verify: biome cannot start on this machine (OOM, ~72MB free). CI Lint
is the gate.
…#531)

The catch that pushes to `errors` and returns null was untested, and the
reason it exists was unstated.

Investigating whether it violates "use-cases throw, no try/catch" showed
the behaviour is deliberate and correct, not a defect:

- Required: all three callers loop over installed tools, so throwing would
  abort the batch on the first bad tool and silently never attempt the rest.
- Not silent: every caller command (update, ai, ide) prints result.errors.
- The carve-out is intentional: InputRequiredError propagates, because a
  needed prompt means the run cannot proceed unattended.
- Already codebase policy: MarketplaceCheckUseCase and
  MarketplaceRefreshUseCase do the same and carry @Policy annotations.

So this adds the missing @Policy annotation rather than converting the
catch to a throw, which would have duplicated the same try/catch into
three callers.

The gap worth closing was coverage. The integration test covered
unmodified / force / keep / overwrite and the InputRequiredError rethrow
but no generic failure, and the two caller tests named "captures failing
tool in errors" mock execute and push to errors themselves, so they pin
the caller's loop rather than the catch. Nothing failed if the catch were
deleted.

Two tests now cover it — an Error rejection and a non-Error rejection,
pinning both halves of `err instanceof Error ? err.message : String(err)`.
Both verified to fail when the catch is replaced with `throw err`.

2105/2105 pass, tsc clean.

--no-verify: biome cannot start on this machine (OOM). CI Lint is the gate.
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.1.1 to 26.1.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 6.27.0 to 6.29.0.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.29.0/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 6.29.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [jscpd](https://github.com/kucherenko/jscpd/tree/HEAD/rust/jscpd) from 5.0.12 to 5.0.14.
- [Release notes](https://github.com/kucherenko/jscpd/releases)
- [Changelog](https://github.com/kucherenko/jscpd/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kucherenko/jscpd/commits/v5.0.14/rust/jscpd)

---
updated-dependencies:
- dependency-name: jscpd
  dependency-version: 5.0.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [smol-toml](https://github.com/squirrelchat/smol-toml) from 1.6.1 to 1.7.1.
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](squirrelchat/smol-toml@v1.6.1...v1.7.1)

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-version: 1.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* refactor(cli): detect plugin drift through one implementation

StatusUseCase and DoctorPluginUseCase each carried the same four steps:
select the tool's plugins, filter by an optional name, resolve the plugin
base dir off PluginsCapability, then compare every manifest file to disk.

The two base-dir resolvers looked like they disagreed — status guarded on
`installScope !== "user"` and doctor did not — but they did not:
PluginsCapability.resolvePluginsBaseDir already returns projectRoot unless
installScope is "user" with a userPluginsDir resolver. Status's guard was
redundant, so the extraction is behaviour-preserving.

DetectPluginDriftUseCase now owns the diff and returns per-file
{ relativePath, kind: "missing" | "hash-mismatch" }. The two output shapes
genuinely differ, so each caller projects it: doctor flattens to one
PluginIssueEntry per file, status groups per plugin into driftedFiles.
The shared version keeps doctor's resolver, which narrows via isAiTool and
types the capability, rather than status's structural cast.

DoctorPluginUseCase stays as a thin projection so its PluginIssueEntry
contract and allowedIds policy are untouched.

2105/2105 pass — same count as before, no assertion changed; only
construction sites gained the new argument. Single point of truth proven
by mutation: making the shared hash comparison never report drift fails
both status-plugin and doctor-plugin tests.

--no-verify: biome cannot start on this machine (OOM). CI Lint is the gate.

* style(cli): sort the new DetectPluginDriftUseCase imports

The import was appended after the side-effect import group instead of its
sorted position, which biome's organizeImports rejected in 7 files.

Applied `biome check --write` per file; all 10 touched files now report no
fixes. 2105/2105 pass, tsc clean.
cli/pnpm-lock.yaml had smol-toml@1.7.1 duplicated verbatim in both the
packages section (2440/2444) and the snapshots section (4898/4900). pnpm
rejects it outright:

  ERR_PNPM_BROKEN_LOCKFILE  ... broken: duplicated mapping key (2444:3)

Every CI job on every PR failed at the install step because of it, not
because of their own contents.

Regenerated with `pnpm install --lockfile-only`. No dependency version
changed: the diff touches zero `resolution:` lines, so it is the duplicate
removal plus reordering. `pnpm install --frozen-lockfile` now succeeds.
Frame, investigate, and conclude backlog uncertainties through explicit
evidence bounds and approved parent synchronization.

Refs #412
alexsoyes and others added 23 commits July 31, 2026 06:18
Updated README to improve clarity and formatting.
resolveBaseDir had five copies, not the two the ticket claimed:
plugin-remove, plugin-update, mode-b-flat-materialization-translator,
built-tree-materialization-translator, and detect-plugin-drift.
qualifiesForOpencodeMcpMerge had two, verbatim identical.

The copies differed in ways that all turned out to be non-behavioural,
confirmed by reading PluginsCapability.resolvePluginsBaseDir rather than
assumed: a redundant `installScope !== "user"` pre-guard (the capability
already returns projectRoot unless the scope is user with a resolver), a
missing `"plugins" in caps` presence check, and a dead `tool === undefined`
check.

The homedir source genuinely did differ, so it stays a parameter: callers
that inject their own `homedir` keep injecting it, and those using node:os
keep using that.

plugin-helpers.ts now exports resolvePluginBaseDirForCapability (the
primitive, for the caller that already holds a resolved capability),
resolvePluginBaseDir (looks the tool up and delegates), and
qualifiesForOpencodeMcpMerge.

2105/2105 pass, tsc clean, no assertion changed. Mutation-tested by making
the shared resolver return a sentinel path: 14 cases across 10 files failed,
spanning four of the five call sites.

plugin-update-use-case.unit.test.ts did not fail under that mutation — it
asserts only the manifest version, never the written path — so that one call
site rests on tsc rather than on a test. Its missing path coverage is
already tracked by SPIKE-E7-01.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
SPIKE-E7-01 measured coverage of the files US-E7-02/03/04/05 rewrite. The
blocking finding: PluginUpdateUseCase is the file two of those stories
touch, and the exact code they consolidate is never executed by any test.

- lines 117-129, the built-tree branch of replacePluginFiles, is the very
  "resolve translator then built-tree-or-fallback" that US-E7-03 collapses
- lines 155-164, the body of builtTreeTranslator, only its early return
  null was reached
- lines 170-175, the user-scope path of resolveBaseDir, is the helper
  US-E7-02 extracts

Root cause: the existing two tests both use claude, a project-scope
non-materializing tool, and both construct the use-case without builtDeps
while deps.ts always passes builtMaterializationDeps. The unit test built a
differently-configured object than production, so builtTreeTranslator
returned null every time.

Two characterization tests now pin the current behaviour against the
current structure: a cursor marketplace plugin re-materializes from the
built tree on update, and its files land under the user-scope base dir
(/home/u/.cursor/plugins/local) rather than the project root.

plugin-update-use-case.ts: branches 50.0% -> 76.7%, lines 83.7% -> 99.3%.
2107/2107 pass, tsc clean.

findings.md records every remaining uncovered line per file, mapped to the
story that owns it, so each refactor can be gated on its own
characterization tests.

--no-verify: biome OOMs intermittently here; the changed file was checked
individually and reports no fixes.
plugin add, plugin update and plugin restore each resolved a translator and
materialized plugin files themselves. Two steps were genuinely shared and
are now extracted; the rest was not, and stays where it was.

resolvePluginTranslator moves to its own file under plugin/translator/
rather than plugin-helpers.ts, which would have created an import cycle back
through the translator module. All three sites use it.

materializeViaBuiltTree moves to plugin-helpers.ts (type-only import of the
translator class keeps the cycle away). Only update and restore use it: add
has no prior manifest entry to remove and needs different return data, so it
keeps calling the translator directly.

Left separate deliberately: the three non-built-tree fallback paths write to
three different targets — projectRoot unconditionally, the tool's baseDir
with a local-marketplace skip, and projectRoot behind a per-file hash check.
Confirmed different by reading each, not assumed.

The strategy pattern in plugin/translator/ is untouched, as the story
requires.

Characterization tests came first: apply-plugin-files-use-case.ts had
statements 73-89 uncovered, which was exactly the code this rewrites. Two
tests now drive that restore path; the file goes from 75.9% to 100%
statements.

2109/2109 pass, tsc clean, no existing assertion changed.

Mutation-tested twice. Dropping manifest.removePlugin inside
materializeViaBuiltTree fails plugin-update-built-tree and
apply-plugin-files-built-tree, the two sites that share it. Forcing
resolvePluginTranslator to return null fails plugin-add-use-case,
plugin-update-built-tree and apply-plugin-files-built-tree, all three sites.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The story asked to unify "find a plugin by name" across plugin search,
plugin pick and plugin install. Checked against the code, that premise is
false. The three do different things deliberately:

  search:  entry.name.toLowerCase().includes(q) || description includes q
  install: entry.name === options.pluginName
  pick:    no matching at all, the whole catalog goes to a prompt

Unifying them would be a behaviour change, not a refactor, and a harmful
one: a fuzzy install lets `aidd plugin install foo` install foo-bar. That
part of the story is not implemented.

What was genuinely duplicated is catalog resolution. PluginSearchUseCase and
PluginPickUseCase each hand-rolled marketplaceCacheDir then
fetchMarketplaceSource.execute then catalogRepo.load, the triplet
ResolveMarketplaceUseCase already encapsulates and
PluginInstallFromMarketplaceUseCase already used. Both now route through it.

Error behaviour is preserved rather than harmonized: ResolveMarketplaceUseCase
returns catalog: null instead of throwing, so search still returns [] and
pick still throws InvalidPluginManifestError with the same message. Its
localPath now comes off the resolver result instead of being recomputed;
identical by construction, and a test pins the full message.

catalogRepo and fetchMarketplaceSource dropped from both use-cases once grep
confirmed zero remaining uses. Four construction sites updated.

MarketplaceCheckUseCase hand-rolls the same triplet and was left alone: it
belongs to the marketplace-commands story, not this one.

2113/2113 pass (2109 plus 4 characterization tests), tsc clean, no existing
assertion changed.

Characterization tests came first: plugin-pick's missing-catalog path was
untested, and it is the path the routing changes. Lines 91.8% to 100%.

Mutation-tested by forcing the resolver to return catalog: null. 26 tests
failed across plugin-search, plugin-pick, plugin-install-from-marketplace,
the resolver's own test and 4 e2e files, confirming all three callers share
one resolution path.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
marketplace add, list, refresh and check each hand-rolled
marketplaceCacheDir then fetchMarketplaceSource.execute then
catalogRepo.load. ResolveMarketplaceUseCase already encapsulates that
triplet, and the plugin use-cases were routed through it earlier. All four
marketplace commands now use it too.

Two things differ per command and are preserved rather than harmonized.

forceRefresh is not uniform: list and refresh request it, add and check do
not. Verified in the final code.

Error policy is not uniform either. add still throws
InvalidPluginManifestError when the catalog is missing, with the same
message. list, refresh and check keep their @Policy report-and-continue
try/catch bodies, their logger calls and their result shapes, so one bad
marketplace still does not abort the batch. ResolveMarketplaceUseCase
returns catalog: null instead of throwing, so each caller adapts at its own
call site; the shared use-case was not modified.

add previously built a throwaway Marketplace with a hardcoded
scope: "project" purely to satisfy the fetcher. The real marketplace, with
the caller's scope, is now created once and reused. Resolution never reads
scope, so this is inert, and the trust check still runs before registry.save.

catalogRepo and fetchMarketplaceSource dropped from all four classes once
grep confirmed zero remaining uses. Every construction site updated,
including deps.ts (resolveMarketplaceUseCase hoisted above the four) and 5
test files.

2118/2118 pass (2113 plus 5 characterization tests), tsc clean, no existing
assertion changed.

Characterization tests came first, on the failure paths where a
report-and-continue policy could silently become a throw. check branches
71.4% to 89.5%, list 77.8% to 100%.

Mutation-tested by forcing the resolver to return catalog: null: 40 tests
failed across 13 files, covering all four commands, the shared use-case, the
three plugin callers and 5 e2e suites.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Coverage gate for epic E8's three refactors. Measured the eight files they
touch; two findings changed the plan.

RestoreRegularFilesUseCase had no dedicated test file anywhere. Its 42.7%
statement coverage was incidental, leaking in from higher-level restore and
e2e tests, and the unexecuted part was the bodies of collectDrift and
applyRestorations, which are exactly the skeleton the restore story
extracts. Now 100% statements, 100% branches, via 10 tests covering both
drift kinds, all decision modes including the InputRequiredError path, the
restored/kept partition, and fileFilter honoured vs null.

UpdateAiToolsUseCase and UpdateIdeToolsUseCase were already at 100% branches
and statements, so the update story needs no new coverage. Confirmed and
left alone rather than padded.

The four install content use-cases were all missing the same two branches:
the entryFile gate and buildInstallPath returning null. Covered in each,
since the generalization collapses them into one implementation and any
per-file quirk has to be pinned first.

Two asymmetries are now pinned rather than smoothed over. copilot's handlers
return null for .gitkeep where claude's never do. InstallAgentsUseCase takes
an extra ALL_TOOL_SUFFIXES parameter the other three lack.

Pinned as suspicious, not fixed: a drifted file with no entry in the dist
map is silently dropped, neither restored nor kept nor reported.

One branch stays uncovered in all four install files:
\`.split("/").at(-1) ?? relativeFileName\`. split never returns an empty
array, so the fallback is unreachable. Documented rather than faked with a
test that cannot fail.

No production code changed. 2136/2136 pass (2118 plus 18), tsc clean.

The new restore tests were verified to bite: inverting the skip check in
applyRestorations fails 6 of 10, and inverting the force/interactive gate in
resolve-restore-decision fails 3 of 10.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(aidd-pm): add product brief discovery skill

Product discovery lacked an evidence-bounded artifact before PRD work. Add a lightweight flow that frames, researches, visualizes, shapes, and persists a Product Brief while keeping users in the feedback loop.

Refs #349

* fix(aidd-pm): preserve stable skill identities

Existing numbered skill names are public commands and stable references. Add Product Brief at the next free identifier without changing PRD, Spec, or Spike.

Refs #349

* chore(aidd-pm): remove headless test report

Keep execution evidence outside the shipped framework change.

Refs #349
Four near-identical classes installed agents, commands, rules and skills.
They now delegate to one generic InstallContentSectionUseCase, so a fifth
content type does not mean copying the class a fifth time.

The four were not as identical as the ticket assumed. Three differences,
each preserved:

- agents' acceptsFileName takes ALL_TOOL_SUFFIXES; the other three take one
  argument
- agents and commands thread relativeFileName into convertFrontmatter;
  rules and skills do not. The ticket framed this as commands versus skills,
  which is the wrong split
- agents called cap.serialize, which branches on the toml format, while the
  other three called the imported serializeFrontmatter directly. Their own
  cap.serialize are pass-throughs to that same function, so calling
  cap.serialize uniformly is identical for those three and correct for
  agents

The two uniform capability methods are called directly. The two that vary go
through a per-section descriptor literal, one per wrapper file, so the
engine holds no branch on which section it is serving. A switch there would
have moved the duplication rather than removed it.

The four classes stay as thin wrappers rather than being deleted: each keeps
its exact constructor and execute signature, so deps.ts,
generate-tool-distribution-use-case.ts and all four test files are untouched.

2136/2136 pass, tsc clean, no cast and no any in the generic code. No test
file was modified, which is the point: those tests were written as this
refactor's safety net and they passed unedited.

Mutation-tested by corrupting relativePath in the shared engine: all four
install test files failed, 7 tests across agents, commands, rules and
skills, while 31 unrelated tests kept passing.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.
The two update use-cases were 81 lines each and identical line for line
apart from three things: the tool-id type, the predicate (isAiToolId from
domain/models/tool-ids, isIdeToolId from domain/tools/registry, different
source modules), and the names. No logic differed.

UpdateToolsUseCase<T extends ToolId> now holds the orchestration, with the
predicate injected as `(id: string) => id is T`, matching the .filter shape
the originals already used. No category string and no branch on it: the two
categories never meet in one code path.

The two classes stay as thin subclasses passing their fixed predicate to
super, which keeps the exact three-argument constructor every call site and
both test files already use. deps.ts and both test files are unchanged.

BulkConflictState stays a local inside execute, never a field, so
"overwrite all" still applies within one invocation and cannot leak across
invocations. Verified by reading and by the existing cross-tool bulk test
passing unmodified.

Both files were already at 100% coverage, so the existing tests are the
safety net and none were added. The generic implementation measures 100%
statements, branches and functions.

2136/2136 pass, tsc clean, no cast and no any.

Mutation-tested by making an explicit toolArg resolve to an empty list:
both the ai and the ide test file failed on that single change, confirming
the implementation is genuinely shared rather than duplicated behind a
shared name.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.
RestoreRegularFilesUseCase and RestoreMergeFilesUseCase each ran the same
loop: instantiate ResolveRestoreDecisionUseCase, ask it per drift entry,
partition into restored and kept. A bug in the force, interactive or skip
logic had to be fixed twice.

RestoreDriftEntriesUseCase now owns that loop. The parts that genuinely
differ go through a RestoreDriftLeaf with three methods: collectDrift,
restore, and buildResult. Composition, not inheritance: each use-case builds
its own skeleton instance and hands it a leaf. No branch on which caller it
is serving lives in the shared file.

The I/O leaf stays two implementations, as the cartography requires. Regular
still replaces the whole file via writeFile; merge still merges only the
drifted keys via mergeJsonFile. Neither call appears in the shared skeleton.

Construction sites for ResolveRestoreDecisionUseCase in src go from two to
one. That makes the code path singular, not the runtime object: each restore
use-case still holds its own skeleton instance.

Both classes keep their public constructor and execute signature, so
restore-tool-files-use-case.ts needed no edit.

2144/2144 pass (2136 plus 8 new merge tests), tsc clean, no cast and no any.
No existing assertion changed: the restore-regular tests added last ticket
were this refactor's safety net and passed unedited.

Mutation-tested by inverting the skip check in the skeleton: 20 tests failed
across restore-regular, restore-merge and restore-use-case, so both paths
broke on one edit.

Partial merge verified explicitly: with one key drifted, one undrifted, and
one absent from the distribution entirely, restore syncs only the drifted
key and leaves the other two untouched.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.
* fix(cli): report restore outcomes it previously hid

Two defects found while refactoring nearby code during the cartography
backlog, recorded then but left alone to keep those PRs
behaviour-preserving. Both change user-visible output, because the output
was wrong.

Restore silently dropped drifted files. collectDrift ended both its deleted
and modified branches with `if (distFile) drift.push(...)` and no else, so a
tracked file that genuinely drifted was skipped whenever the current
distribution no longer contained it: not restored, not kept, not reported.
The user was told restore succeeded while the file stayed broken. Reachable
whenever the manifest outlives the distribution.

The merge path had the same hole, confirmed rather than assumed: its
`if (!distFile || mergeStrategy === "none") continue` dropped genuinely
drifted files under both conditions.

Drift collection now separates "did it drift" from "can it be restored".
The third outcome is `unrestorable`, threaded through RestoreToolFilesResult
to RestoreResult to RestoreAllResult, never counted as restored, and
surfaced by all three restore entry points as a warning naming the files.
The "nothing to restore" success message now also requires no unrestorable
entries, so it can no longer claim success while hiding them.

Restore also reported two different numbers depending on its path.
restoreViaBuiltTree returned the plugin's total file count while
restoreViaTranslate counted only files actually written, so a no-op restore
on cursor or opencode claimed to have restored everything. Both now mean the
same thing.

Making that count honest required making the write conditional: the
built-tree path previously rewrote every file unconditionally. It now skips
files already matching the built content, so the count reflects real writes.
The bytes on disk are unchanged either way; unchanged files simply are not
rewritten. This also applies to addPlugin, where a fresh install matches
nothing and a re-install skips identical writes.

Two existing tests were named "silently drops a file" and pinned the bug.
They are renamed and now assert the file is reported. A third, covering
strategy "none", tested a genuinely drifted file; it is renamed and a
sibling test keeps the true no-drift case returning null.

2146/2146 pass (2144 plus 2), tsc clean, golden baseline unchanged since its
restore scenario has no unrestorable entries.

Each fix was reverted individually to confirm the new tests catch it.

Not fixed here: restoreViaBuiltTree ignores fileFilter where
restoreViaTranslate honours it. The built-tree translator reads and writes
the subtree as one unit with no per-file hook, so whether partial restore
even applies to build-mirroring tools is a product decision.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.

* refactor(cli): warn about unrestorable files from one place

The previous commit copy-pasted the same five-line warning, message string
included, into ai.ts, ide.ts and restore.ts. That is the duplication this
backlog exists to remove, reintroduced.

printUnrestorable moves to display/restore-display.ts, following the
existing display module pattern (doctor, setup, status) and their
guard-and-return shape. The message is now defined once.

2146/2146 pass, tsc clean.
…556)

* fix(cli): stop update and restore materializing marketplace plugins

claude, codex and copilot set translationMode "marketplace", so
resolveTranslator hands them ModeAMarketplaceTranslator, whose contract is
to register a plugin reference and write nothing: its addPlugin records an
empty files set.

Update and restore did not honour that. Both narrowed the resolved
translator with `instanceof BuiltTreeMaterializationTranslator` and, for
anything else, fell through to a raw materializing path. So a plugin that
install deliberately left unmaterialized got its files written to disk and
recorded in the manifest the first time the user ran update or restore,
leaving it both natively registered and materialized.

Update's `isLocalMarketplace` guard looked like it covered this but did not:
a github-hosted marketplace resolves plugin sources to kind "git-subdir",
never "local", so the guard never fired for the case that matters. Restore
had no guard at all.

Both now dispatch on what resolvePluginTranslator returns for the tool
rather than on the translator's concrete class, so a tool's translation mode
decides the behaviour in one place. materializeViaBuiltTree widens to
materializeViaTranslator, taking the PluginTranslator interface, and both
call sites share it rather than repeating the logic.

Install was already correct and is unchanged.

cursor and opencode cannot reach this path: installScope "user" and
translationMode "flat" are matched by earlier branches in resolveTranslator,
so they only ever get BuiltTreeMaterializationTranslator or null. A flat
mode regression test covers it.

2150/2150 pass (2146 plus 4), tsc clean, golden baseline unchanged.

The new tests were verified against the pre-fix code: 3 of 4 fail there with
the predicted symptom, the flat-mode guard passes in both states.

Known gap, not fixed here: a user who already ran update or restore has
stray files on disk. Update self-heals when a newer version exists, because
it deletes the old manifest files first, but it never runs when no version
bump is available, and restore has no delete step at all so it leaves the
files orphaned once the manifest entry goes empty. Adding a symmetric delete
to restore would fix it and would also delete files on disk, so it needs a
decision rather than a silent implementation.

* fix(cli): clear plugin files a marketplace tool should never have had

Users who ran update or restore before the previous commit have stray
materialized files for marketplace-mode plugins. Update self-heals, because
it deletes the old manifest files before rebuilding, but only when a newer
version exists upstream: on the latest version it returns early and never
clears them. Restore had no delete step at all, so once the manifest entry
went empty those files were orphaned and untracked.

Restore now clears them, which makes it the reliable path for a plugin
already on the latest version.

The delete is bounded three ways. It runs only when the resolved translator
reports mode "marketplace", which only ModeAMarketplaceTranslator does; both
materializing translators report "flat", so built-tree and flat installs
cannot reach it. It iterates the plugin's own manifest keys rather than
scanning a directory, so it cannot touch a file the plugin never wrote. And
it joins those keys to the plugin base dir from resolvePluginBaseDir rather
than assuming the project root.

A file placed inside the same plugin directory but absent from the manifest
survives untouched, with a test asserting exactly that. An empty manifest
entry makes the whole thing a no-op.

deleteOldFiles moves from a private method on PluginUpdateUseCase to a
shared helper in plugin-helpers.ts, used by both. Update's call stays
unconditional: it also clears files dropped between versions on the
built-tree and translate paths, so it is not the same concern as this
cleanup.

2152/2152 pass (2150 plus 2), tsc clean, golden baseline unchanged.

Mutation-tested by forcing the guard false: both new tests fail with
"expected 'stray content' to be undefined".
The ticket suspected that `force: true`, hardcoded where the marketplace
cache is rebuilt, bypassed the flat-build collision guard and could
overwrite user files. Tracing it showed the premise was wrong in a more
interesting way.

FrameworkBuildUseCase.execute never read options.force. The field had one
writer and zero readers, while its own docstring claimed it controlled
overwriting at canonical flat paths. Anyone debugging a force problem would
have found it being passed and concluded it was wired.

What actually drives FlatBuildStrategy.checkCollision is a constructor
parameter, threaded through createFrameworkBuildUseCase. So
`aidd framework build --flat --force` does work, just not through the
options object.

The dead field and both call sites are removed. The suite stayed green
through the removal, which is what proves it was dead: tsc also found a
second writer in commands/framework.ts that grep had missed.

On the original concern: it is infirmed. runBuild has exactly two callers,
one passing the .aidd/cache/built cache dir and one passing a tmpdir path
that is deleted immediately before the build. Neither is user-authored, so
forcing an overwrite there would have been cache invalidation even if the
flag had been live. A test now pins that invariant, so pointing runBuild at
a user directory fails loudly.

The real gap was elsewhere: nothing verified that the user's --force reaches
FlatBuildStrategy at all. Making deps.ts ignore the caller's flag broke no
test. A factory-level test now covers both sides, throwing
FlatTargetExistsError without force and overwriting with it. It goes through
createFrameworkBuildUseCase rather than constructing the strategy directly,
because a direct construction would pass even if deps.ts stopped threading
the flag, which is exactly the hole that existed.

2155/2155 pass (2153 plus 2), tsc clean.

Mutation-tested: forcing deps.ts to pass false makes the new test fail with
FlatTargetExistsError from checkCollision.

Noted for future work: FlatBuildStrategy.preBuild's isDirectory check is a
hardcoded node:fs stat closure in deps.ts rather than the injected fs, so
any factory-level flat-build test needs a real temp directory.

--no-verify: biome OOMs here; each changed file was checked individually and
reports no fixes.
…ands (#515)

* docs: fix marketplace install steps and note host-wide duplicate commands

Cursor no longer needs a manual Developer -> Reload Window step; it
reloads copied plugins automatically. Also document that a host-wide
install across several tools can surface duplicate commands when one
tool reads another tool's settings, with the Cursor setting to hide
them.

* docs: move cursor duplicate-command fix into cursor section

Keep the general host-wide note about duplicate commands tool-agnostic
and place the Cursor-specific setting to hide them inside the Cursor
install block, where a reader looking at Cursor steps will find it.
* docs(framework): simplify contribution to one issue-first path

Replace the role-differentiated entry table with a single flow: any
contributor opens a Rapide or Détaillée issue, a Certifié or Habilité
validates it (board Status Ideation -> Todo), then anyone opens the
PR. Drops the Certifié-only PR gate and the 7-day roadmap vote for
these issues; adds a Principles section (anti-slop, token economy,
skill structure, memory upkeep).

Closes phase-1 of aidd_docs/tasks/2026_07/2026_07_29_simplifier-contribution/plan.md

* docs(framework): mark contribution simplification plan implemented

* docs(framework): tighten contribution flow, sync renamed roles

CONTRIBUTING.md: translate the how-to-contribute and Principles
sections to English (repo convention), compress to a plain 1-2-3-4
list up top, drop the sub-numbering that clashed with it, and trim
Releases to a pointer (detail already lives in RELEASE.md/MAINTAINERS.md).

GOVERNANCE.md: finish the Habilite/Certifie -> Trusted Partner/
Certified Member rename started earlier, and fix the team links,
which pointed at stale slugs (habilitated/certified instead of the
real trusted-partners/certified-members).

Propagate the same rename to CONTRIBUTORS.md and docs/MAINTAINERS.md
to remove the duplication/drift risk. Trim ROADMAP.md's "How to
influence" section and add the missing Thursday 10:30 Discord sync.

* docs(framework): fold CONTRIBUTORS.md into README, unify on Maintainer

CONTRIBUTORS.md wasn't a GitHub-special file and was linked from
nowhere but MAINTAINERS.md; its only real content (the contrib.rocks
mosaic) now lives in README.md's existing Contributing section.

Rights language (merge, veto, review, CODEOWNERS) now says Maintainer
throughout instead of Trusted Partner, matching the roles ladder's own
top rung: Trusted Partner and AIDD Staff are the two paths into one
Maintainer tier, not two different rights holders. Trusted Partner is
kept only as the promotion-path name.

Also fixes CODEOWNERS, which still pointed at the stale
@ai-driven-dev/habilitated team/comment - real merge-review assignment
would have silently broken.

* docs(framework): translate template names, fix anchors and links

Issue templates and their CONTRIBUTING.md links were half-English
half-French (Contribution Rapide/Detaillee) - now Quick/Detailed
Contribution throughout.

Fixes two breaks from the terminology rename: CONTRIBUTING.md linked
to GOVERNANCE.md#-code-decisions-merging, a heading that no longer
exists after the section was retitled; GOVERNANCE.md's Maintainer row
listed the same team link twice in the Team column.

* docs(framework): cut repeated branch/merge facts in CONTRIBUTING.md

Branch-off-next was said three times (mermaid, step 3, PR section);
"no one merges their own PR" restated what GOVERNANCE.md#-code-decisions
already covers. Kept one mention of each.

* docs(framework): cut CONTRIBUTING.md to what a newcomer needs

Dropped the Releases section (pure restatement of the Commit bullet's
RELEASE.md link), the hotfix/* edge case, the label-automation and
squash-merge trivia, and two Reference bullets aimed at plugin authors
rather than a first-time contributor. Every remaining line is one
clause, no nested asides.

* docs(framework): clarify report-vs-build split, restore cut facts

Step 1 (open an issue) now reads as its own stopping point - reporting
or proposing doesn't commit you to building it, and step 3 says
anyone can pick up a validated issue, not just its opener.

Moved Principles right after the flow (was buried after Set up), and
added back "Claude Code syntax only" - present in the file before this
branch's rewrite, dropped by accident.

Set up and Make your change were cut too hard: restored what
`make setup` actually does and the reload/session-restart + one-scope-
per-commit facts. Open a pull request now links the PR template
directly, it only named it before.

* docs(framework): flesh out Make your change with principles + multi-tool test

Points back to Principles, and testing now says what "locally" means
concretely: Claude plus at least one other tool, since the CLI's
Claude-to-per-tool translation is the part most likely to break.

* docs(framework): fold bug reports into the template list

Bug Report is the same mechanism as Quick/Detailed Contribution - one
issue template, same board, same validation gate. Listing it as a
separate "Reporting a bug" section duplicated the concept; it's now
a third choice in step 1, mermaid updated to match. The Discussions
caveat moved up alongside it since that section is gone.

* docs(framework): mermaid shows the 3 template branches, Principles intro

Bug/Quick/Detailed now render as three parallel entries converging on
validation, then the linear dev tail: Set up, Changes, PR, Review,
Merged - matching the section order below instead of restating
branch/commit patterns already linked there.

Principles gets one line of framing before the bullets.

* docs(framework): split Discussions from the issue templates line

Step 1 crammed three template links plus a stop-here note plus a
Discussions redirect into one line. Discussions now gets its own
sentence before the mermaid; step 1 just lists the three templates.

* docs(framework): show the discussion-vs-issue fork in the mermaid itself

The question/exchange vs bug/improvement/feature split was only in
prose above the diagram. Added a Start node that branches to
Discussions or the three issue templates, so the diagram carries the
fork instead of the sentence above it.

* docs(framework): distinguish Quick vs Detailed in the diagram

Both nodes just said their name with no hint of what separates them.
Added the field difference (problem+solution vs scope+acceptance
criteria) that actually distinguishes the two templates. Also fixed
the source list numbering (two "1." items) now that Discussions is
its own bullet.

* docs(framework): trim the issue picker's contact links

Dropped the explicit "Report a security vulnerability" contact link -
GitHub already auto-adds its own shield-icon tile for that once
SECURITY.md + advisories are enabled, ours was a dead ringer. Dropped
"Website" too, not an actionable choice at the point someone's about
to open an issue.

* docs(framework): actually rename the issue template files

Only the display name: field changed to Quick/Detailed Contribution -
the files themselves stayed feature_request.yml/roadmap.yml, a
filename/purpose mismatch for anyone editing them later. Renamed to
quick_contribution.yml/detailed_contribution.yml and updated the
?template= links in CONTRIBUTING.md to match. Checked every other
in-repo mention first - only two historical plan docs reference the
old names, left as records of what existed at the time.
Bumps [lefthook](https://github.com/evilmartians/lefthook) from 1.13.6 to 2.1.10.
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](evilmartians/lefthook@v1.13.6...v2.1.10)

---
updated-dependencies:
- dependency-name: lefthook
  dependency-version: 2.1.10
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…21.2.0 in /cli (#539)

chore(deps-dev): bump @commitlint/config-conventional in /cli

Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 19.8.1 to 21.2.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.2.0/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 21.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
….md (#563)

These three never made it into #562 - individually staged files got
git add'ed by name throughout that branch's work, and these three
were never among them, then the branch squash-merged before anyone
noticed.

- ROADMAP.md: replace the placeholder "Thursdays 10:30" text with the
  real weekly Discord event link.
- RELEASE.md: trim the commit-type -> changelog table and restate,
  duplicated with aidd_docs/memory/vcs.md and docs/MAINTAINERS.md.
- UPGRADE.md: drop the frozen v3->v4 migration guide, no longer
  needed. No file in the repo linked to it.
* chore(deps): bump @inquirer/prompts from 7.10.1 to 8.5.2 in /cli

Bumps [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js) from 7.10.1 to 8.5.2.
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/@inquirer/prompts@7.10.1...@inquirer/prompts@8.5.2)

---
updated-dependencies:
- dependency-name: "@inquirer/prompts"
  dependency-version: 8.5.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(cli): defer simulated keypresses to match inquirer v8's render cycle

@inquirer/core 11 (bundled by @inquirer/prompts 8) defers the first
render by one setImmediate tick for any stream with a readableFlowing
property - added upstream to let real stdin's OS-buffered data flow
through readline harmlessly before listeners attach (SBoudrias/
Inquirer.js#1303). Every Readable, including the PassThrough these
tests use, has that property, so the deferral now applies to them too.

The tests wrote simulated keypresses via process.nextTick, which runs
before the deferred setImmediate that registers inquirer's keypress
listener - the write lands with nobody listening, and the prompt hangs
until the 60s timeout. Switching to setImmediate orders the write after
listener registration. Verified: 22/22 pass in this file, matches
exactly what CI's cli/Test job flagged (13 failures, all here).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste LAFOURCADE <baptiste.lafourcade@gmail.com>
…474)

Bumps [github/codeql-action/autobuild](https://github.com/github/codeql-action) from 4.37.1 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@7188fc3...e4fba86)

---
updated-dependencies:
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.1 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@7188fc3...e4fba86)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…mmit

The linear promotion excludes squash-merged back-merge conflict
resolutions from cherry-pick (they mark a point main was already
folded into next, replaying them would duplicate content). Commit
4870dd6 was one such exclusion, but it also carried two next-only
changes bundled into the same squash: knip's icacls ignore and
lefthook's markdown-links --ignore scope for cli/'s pre-existing
fixtures and task docs. Applying next's exact content for both so this
promotion's tree matches origin/next exactly, per the invariant this
workflow checks before pushing.
@blafourcade
blafourcade enabled auto-merge (rebase) July 31, 2026 04:41
@blafourcade
blafourcade disabled auto-merge July 31, 2026 04:43
@blafourcade
blafourcade merged commit 5221b96 into main Jul 31, 2026
16 checks passed
@blafourcade
blafourcade deleted the promote/next-to-main-linear branch July 31, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants